/**
	The Minerva Project, DAG Scheduling Tools
	Copyright (C) 2006 Florin Pop & Stefan Bucur

	This program is free software; you can redistribute it and/or modify
	it under the terms of the GNU General Public License as published by
    the Free Software Foundation; either version 2 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.
 */
package ro.pub.cs.minerva.monitoring;

import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.GradientPaint;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Paint;
import java.awt.Point;
import java.awt.RenderingHints;
import java.awt.event.ComponentEvent;
import java.awt.event.ComponentListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.awt.geom.AffineTransform;
import java.awt.geom.Rectangle2D;

import ro.pub.cs.minerva.resource.Resource;
import ro.pub.cs.minerva.scheduler.TaskMapping;
import ro.pub.cs.minerva.scheduler.TaskScheduleResult;

import javax.swing.JComponent;
import javax.swing.JToolTip;
import javax.swing.Popup;
import javax.swing.PopupFactory;

/**
 * @author Stefan Bucur
 *
 */
public class GanttChart extends JComponent implements MouseListener,
	MouseMotionListener, ComponentListener {
	
	/*
	 * PRIVATE FIELDS
	 */
	
	private TaskScheduleResult dataSource = null;
	
	private TaskMapping hoverMapping = null;
	private TaskMapping selectedMapping = null;
	
	private Resource hoverResource = null;
	private Resource selectedResource = null;
	
	private Paint backgroundPaint = null;
	private Paint chartTitlePaint = null;
	
	private String chartTitle = "Gantt Chart";
	private String timeAxisTitle = "Time";
	private String resourceAxisTitle = "Resources";
	
	private boolean needsPaintUpdate = false;
	private boolean needsMetricsUpdate = false;
	
	private GanttChartProperties chartProperties =
		GanttChartProperties.DEFAULT_PROPERTIES;
	
	private GanttChartMetrics chartMetrics = null;
	
	private GanttChartMetrics preferredMetrics =
		GanttChartMetrics.DEFAULT_METRICS;
	
	private Popup toolTipPopup = null;
	private JToolTip toolTip = null;
	
	/* **************************************************
	 * USEFUL ROUTINES
	 * **************************************************/
	
	private Color getLighterColor(Color color, float amount) {
		float[] hsb = new float[3];
		
		Color.RGBtoHSB(color.getRed(), color.getGreen(),
				color.getBlue(), hsb);
		
		hsb[2] += amount; hsb[2] = (hsb[2] > 1.0f) ? 1.0f : hsb[2];
		
		return Color.getHSBColor(hsb[0], hsb[1], hsb[2]);
	}
	
	private Color getDarkerColor(Color color, float amount) {
		float[] hsb = new float[3];
		
		Color.RGBtoHSB(color.getRed(), color.getGreen(),
				color.getBlue(), hsb);
		
		hsb[2] -= amount; hsb[2] = (hsb[2] < 0.0f) ? 0.0f : hsb[2];
		
		return Color.getHSBColor(hsb[0], hsb[1], hsb[2]);
	}
	
	private void inflateRect(Rectangle2D rect,
			float left, float right, float top, float bottom) {
		
		rect.setRect(rect.getX() + left,
				rect.getY() + top,
				rect.getWidth() - left - right,
				rect.getHeight() - top - bottom);
		
	}
	
	private void inflateRect(Rectangle2D rect,
			float amount) {
		inflateRect(rect, amount, amount, amount, amount);
	}
	
	/* **********************************************************
	 * HIT TESTING ROUTINES
	 * **********************************************************/
	
	private int hitMappingIndex(Resource resource, int posX, int posY) {
		if (dataSource == null)
			return -1;
		
		return -1;
	}
	
	private int hitResourceIndex(int posX, int posY) {
		if (dataSource == null)
			return -1;
		
		Rectangle2D mapBounds = getMappingsBounds();
		if (!mapBounds.contains(posX, posY))
			return -1;
		
		Rectangle2D crResBounds = new Rectangle2D.Float();
		
		for (int i=0; i < dataSource.getResourceCount(); i++) {
			crResBounds.setRect(mapBounds.getX(),
					mapBounds.getMaxY() - (i+1)*(int)chartMetrics.getResourceHeight(),
					mapBounds.getWidth(),
					chartMetrics.getResourceHeight());
			
			if (crResBounds.contains(posX, posY))
				return i;
		}
		
		return -1;
	}
	
	/* ***********************************************************
	 * TOOL TIPPING ROUTINES
	 * ***********************************************************/
	
	private void showTip(String text, MouseEvent e) {
		hideTip();

		if (!this.isEnabled() || !this.isShowing()) {
			return;
		}
		
		if (text == null)
			return;
		
		if (toolTip == null) {
			toolTip = this.createToolTip();
		}
		
		toolTip.setTipText(text);
		
		Point tipPos = new Point(e.getX(), e.getY());
		Point absoluteC = this.getLocationOnScreen();
		Dimension dims = toolTip.getPreferredSize();
		
		tipPos.translate(absoluteC.x, absoluteC.y);
		tipPos.translate(0, -dims.height - 1);
		
		toolTip.setBounds(0, 0, dims.width, dims.height);
		
		PopupFactory factory = PopupFactory.getSharedInstance();
		
		toolTipPopup = factory.getPopup(this, toolTip, tipPos.x , tipPos.y);
		
		toolTipPopup.show();
	}
	
	private void hideTip() {
		if (toolTipPopup != null) {
			toolTipPopup.hide();
			
			toolTipPopup = null;
		}
	}
	
	private Rectangle2D getLegendBounds() {
		return new Rectangle2D.Float(
				getWidth() - chartMetrics.getLegendWidth(),
				chartMetrics.getTitleHeight(),
				chartMetrics.getLegendWidth(),
				getHeight() - chartMetrics.getTitleHeight());
	}
	
	private Rectangle2D getResourcesAxisBounds() {
		return new Rectangle2D.Float(
				(getWidth() - chartMetrics.getLegendWidth())/2 - chartMetrics.getChartWidth()/2,
				chartMetrics.getTitleHeight() + (getHeight() - chartMetrics.getTitleHeight())/2 - chartMetrics.getChartHeight()/2,
				chartMetrics.getResourceAxisWidth(),
				chartMetrics.getChartHeight() - chartMetrics.getTimeAxisHeight());
	}
	
	private Rectangle2D getTimeAxisBounds() {
		return new Rectangle2D.Float(
				(getWidth() - chartMetrics.getLegendWidth())/2 - chartMetrics.getChartWidth()/2 + chartMetrics.getResourceAxisWidth(),
				getHeight() - (getHeight() - chartMetrics.getTitleHeight())/2 + chartMetrics.getChartHeight()/2 - chartMetrics.getTimeAxisHeight(),
				chartMetrics.getChartWidth() - chartMetrics.getResourceAxisWidth(),
				chartMetrics.getTimeAxisHeight());
	}
	
	private Rectangle2D getMappingsBounds() {
		return new Rectangle2D.Float(
				(getWidth() - chartMetrics.getLegendWidth())/2 - chartMetrics.getChartWidth()/2 + chartMetrics.getResourceAxisWidth(),
				chartMetrics.getTitleHeight() + (getHeight() - chartMetrics.getTitleHeight())/2 - chartMetrics.getChartHeight()/2,
				chartMetrics.getChartWidth() - chartMetrics.getResourceAxisWidth(),
				chartMetrics.getChartHeight() - chartMetrics.getTimeAxisHeight()
				);
	}
	
	/* **********************************************************
	 * PAINTING ROUTINES
	 * **********************************************************/
	
	/**
	 * 
	 * @param g
	 */
	private void paintBackground(Graphics2D g) {
		g.setPaint(backgroundPaint);
		
		g.fillRect(0, 0, getWidth(), getHeight());
	}
	
	private void paintTitle(Graphics2D g) {
		g.setPaint(chartTitlePaint);
		
		g.setFont(chartProperties.getTitleFont());
		
		FontMetrics fontMetrics = g.getFontMetrics();
		
		float x = getWidth()/2 - fontMetrics.stringWidth(chartTitle)/2;
		float y = chartMetrics.getTitleHeight()/2 + fontMetrics.getHeight()/2;
		
		g.drawString(chartTitle, x, y);
	}
	
	private void paintLegend(Graphics2D g) {
		Rectangle2D bounds = getLegendBounds();
		
		inflateRect(bounds, 5.0f);
		
		// TODO: Customize this
		g.setPaint(Color.WHITE);
		
		g.fill(bounds);
		
		g.setPaint(Color.BLACK);
		
		g.draw(bounds);
	}
	
	private void paintResourcesAxis(Graphics2D g) {
		Rectangle2D bounds = getResourcesAxisBounds();
		
		// Drawing the axis
		g.setPaint(Color.BLACK);
		
		g.drawLine((int)bounds.getMaxX(), (int)bounds.getMinY(),
				(int)bounds.getMaxX(), (int)bounds.getMaxY());
		
		// Drawing the axis title
		
		Font axisTitleFont = chartProperties.getAxesTitleFont();
		FontMetrics titleFontMetrics = g.getFontMetrics(axisTitleFont);
		
		AffineTransform rotTransf = new AffineTransform();
		rotTransf.rotate(-Math.PI/2);
		
		axisTitleFont = axisTitleFont.deriveFont(rotTransf);
		
		g.setPaint(Color.BLACK);
		g.setFont(axisTitleFont);
		
		float x = (float)bounds.getMinX() + titleFontMetrics.getHeight();
		float y = (float)(bounds.getMinY() + bounds.getMaxY())/2 +
			titleFontMetrics.stringWidth(resourceAxisTitle)/2;
		
		g.drawString(resourceAxisTitle, x, y);
		
		// Drawing the resource labels
		
		if (dataSource != null) {
			g.setColor(Color.BLACK);
			g.setFont(chartProperties.getAxesLabelsFont());
			FontMetrics labelsFontMetrics = g.getFontMetrics();
			
			y = (float)bounds.getMaxY() - chartMetrics.getResourceHeight()/2 +
				labelsFontMetrics.getHeight()/2;
			
			for (Resource crResource : dataSource.getResources()) {
				x = (float)bounds.getMaxX() - labelsFontMetrics.stringWidth(crResource.getNodeName());
				
				g.drawString(crResource.getNodeName(), x, y);
				
				y -= (float)chartMetrics.getResourceHeight();
			}
		}
	}
	
	private void paintTimeAxis(Graphics2D g) {
		Rectangle2D bounds = getTimeAxisBounds();
				
		// Drawing the axis
		g.setPaint(Color.BLACK);
		
		g.drawLine((int)bounds.getMinX(), (int)bounds.getMinY(),
				(int)bounds.getMaxX(), (int)bounds.getMinY());
		
		// Drawing the axis label
		
		g.setPaint(Color.BLACK);
		g.setFont(chartProperties.getAxesTitleFont());
		
		FontMetrics titleFontMetrics = g.getFontMetrics();
		
		int x = (int)bounds.getX() + (int)bounds.getWidth()/2 -
		(int)titleFontMetrics.stringWidth(timeAxisTitle)/2;
		
		int y = (int)bounds.getMaxY();
		
		g.drawString(timeAxisTitle, x, y);
	}
	
	private void paintMappings(Graphics2D g) {
		Rectangle2D bounds = getMappingsBounds();
		
		// Drawing the resources
		
		if (dataSource == null)
			return;
		
		Resource[] resources = dataSource.getResources();
		
		for (int i=0; i < resources.length; i++) {
			Rectangle2D resBounds = new Rectangle2D.Double(
					bounds.getX(),
					bounds.getMaxY() - (i+1)*(int)chartMetrics.getResourceHeight(),
					bounds.getWidth(),
					chartMetrics.getResourceHeight());
			
			inflateRect(resBounds, 0, 0, 2, 2);
			
			if (chartProperties.isGradientEnabled()) {
				g.setPaint(new GradientPaint(
						(float)resBounds.getX(), (float)resBounds.getY(),
						getLighterColor(chartProperties.getResourceBackColor(), 0.1f),
						(float)resBounds.getX(), (float)(resBounds.getY() + resBounds.getHeight()),
						getDarkerColor(chartProperties.getResourceBackColor(), 0.1f)
						));
			} else {
				g.setPaint(chartProperties.getResourceBackColor());
			}
			
			g.fillRoundRect((int)resBounds.getX(), (int)resBounds.getY(),
					(int)resBounds.getWidth(), (int)resBounds.getHeight(),
					3, 3);
			
			if (hoverResource == resources[i])
				g.setPaint(Color.RED);
			else
				g.setPaint(Color.BLACK);
			
			g.drawRoundRect((int)resBounds.getX(), (int)resBounds.getY(),
					(int)resBounds.getWidth(), (int)resBounds.getHeight(),
					3, 3);
		}
	}
	
	/**
	 * 
	 *
	 */
	private void updatePaints() {
		
		if (chartProperties.isGradientEnabled()) {
			backgroundPaint = new GradientPaint(
					0, 0,
					getLighterColor(chartProperties.getBackgroundColor(), 0.2f),
					0, getHeight(),
					getDarkerColor(chartProperties.getBackgroundColor(), 0.2f)
					);
			
		} else {
			backgroundPaint = chartProperties.getBackgroundColor();
		}
		
		chartTitlePaint = chartProperties.getTitleColor();
		
		needsPaintUpdate = false;
	}
	
	/**
	 * Implements the algorithm for calculating the metrics for
	 * all the items displayed on the chart.
	 *
	 */
	private void updateMetrics() {
		Graphics g = this.getGraphics();
		
		if (g == null)
			return;
		
		chartMetrics = new GanttChartMetrics();
		
		FontMetrics titleFontMetrics =
			g.getFontMetrics(chartProperties.getTitleFont());
		
		FontMetrics axesFontMetrics =
			g.getFontMetrics(chartProperties.getAxesTitleFont());
		
		FontMetrics labelsFontMetrics =
			g.getFontMetrics(chartProperties.getAxesLabelsFont());
		
		if (preferredMetrics.getTitleHeight() < titleFontMetrics.getHeight()) {
			chartMetrics.setTitleHeight(titleFontMetrics.getHeight());
		} else {
			chartMetrics.setTitleHeight(preferredMetrics.getTitleHeight());
		}
		
		chartMetrics.setLegendWidth(preferredMetrics.getLegendWidth());
		
		chartMetrics.setChartWidth(getWidth() - chartMetrics.getLegendWidth());
		chartMetrics.setChartHeight(getHeight() - chartMetrics.getTitleHeight());
		
		if (preferredMetrics.getChartHeight() != 0.0f &&
				preferredMetrics.getChartHeight() < chartMetrics.getChartHeight()) {
			chartMetrics.setChartHeight(preferredMetrics.getChartHeight());
		}
		
		if (preferredMetrics.getChartWidth() != 0.0f &&
				preferredMetrics.getChartWidth() < chartMetrics.getChartWidth()) {
			chartMetrics.setChartWidth(preferredMetrics.getChartWidth());
		}
		
		chartMetrics.setResourceAxisWidth(preferredMetrics.getResourceAxisWidth());
		
		if (dataSource != null) {
			for (Resource crResource : dataSource.getResources()) {
				Rectangle2D crBounds = 
					labelsFontMetrics.getStringBounds(crResource.getNodeName(), g);
				
				if (crBounds.getWidth() > chartMetrics.getResourceAxisWidth())
					chartMetrics.setResourceAxisWidth((float)crBounds.getWidth());
			}
		}
		
		chartMetrics.setResourceAxisWidth(axesFontMetrics.getHeight() +
				chartMetrics.getResourceAxisWidth());
		
		chartMetrics.setTimeAxisHeight(axesFontMetrics.getHeight() +
				labelsFontMetrics.getHeight());
		
		if (preferredMetrics.getTimeAxisHeight() > chartMetrics.getTimeAxisHeight()) {
			chartMetrics.setTimeAxisHeight(preferredMetrics.getTimeAxisHeight());
		}
		
		if (dataSource != null && dataSource.getResourceCount() > 0) {
			chartMetrics.setResourceHeight((chartMetrics.getChartHeight() -
					chartMetrics.getTimeAxisHeight()) / dataSource.getResourceCount());
			
			if (preferredMetrics.getResourceHeight() != 0 &&
					preferredMetrics.getResourceHeight() < chartMetrics.getResourceHeight()) {
				chartMetrics.setResourceHeight(preferredMetrics.getResourceHeight());
			}
		} else {
			chartMetrics.setResourceHeight(preferredMetrics.getResourceHeight());
		}
		
		needsMetricsUpdate = false;
	}
	
	/**
	 * Updates the chart internal properties and repaints the
	 * component.
	 *
	 */
	private void updateChart() {
		if (needsMetricsUpdate) {
			updateMetrics();		
		}
		
		if (needsPaintUpdate) {
			updatePaints();
		}
		repaint();
	}

	/**
	 * 
	 */
	private static final long serialVersionUID = 200701031524L;
	
	public static final Dimension DEFAULT_PREF_SIZE = new Dimension(600, 400);
	
	public static final Dimension DEFAULT_MIN_SIZE = new Dimension(1, 1);
	
	public static final Dimension DEFAULT_MAX_SIZE = new Dimension(1280, 1024);
	
	
	
	
	/*
	 * MouseListener INTERFACE
	 */

	public void mouseClicked(MouseEvent arg0) {
		// TODO Auto-generated method stub
		
	}

	public void mouseEntered(MouseEvent arg0) {
		// TODO Auto-generated method stub
		
	}

	public void mouseExited(MouseEvent arg0) {
		hideTip();
		
	}

	public void mousePressed(MouseEvent arg0) {
		// TODO Auto-generated method stub
		
	}

	public void mouseReleased(MouseEvent arg0) {
		// TODO Auto-generated method stub
		
	}
	
	/*
	 * MouseMotionListener INTERFACE
	 */

	public void mouseDragged(MouseEvent e) {
		// TODO Auto-generated method stub
		
	}

	public void mouseMoved(MouseEvent e) {
		if (dataSource != null) {
			Resource[] resources = dataSource.getResources();
			
			int hoverResIndex = hitResourceIndex(e.getX(), e.getY());
			
			if (hoverResIndex >= 0) {
				if (hoverResource != resources[hoverResIndex]) {
					hoverResource = resources[hoverResIndex];
					repaint();
				}
				
				int hoverMapIndex = hitMappingIndex(hoverResource, e.getX(), e.getY());
				
				if (hoverMapIndex >= 0) {
					
				}
			} else {
				if (hoverResource != null) {
					hoverResource = null;
					repaint();
				}
				hoverMapping = null;
			}
		}
		
		
		// We must do this after we did the hover tests
		
		String toolTipText = this.getToolTipText(e);
		showTip(toolTipText, e);
	}
	
	/*
	 * ComponentListener INTERFACE
	 */
	
	public void componentHidden(ComponentEvent e) {
		// TODO Auto-generated method stub
		
	}

	public void componentMoved(ComponentEvent e) {
		// TODO Auto-generated method stub
		
	}

	public void componentResized(ComponentEvent e) {
		needsPaintUpdate = true;
		needsMetricsUpdate = true;
		
		updateChart();
		
	}

	public void componentShown(ComponentEvent e) {
		// TODO Auto-generated method stub
		
	}
	
	/*
	 * PUBLIC METHODS
	 */

	

	/**
	 * 
	 */
	public GanttChart() {
		
		addComponentListener(this);
		addMouseListener(this);
		addMouseMotionListener(this);
		
		setPreferredSize(DEFAULT_PREF_SIZE);
		
		setMinimumSize(DEFAULT_MIN_SIZE);
		setMaximumSize(DEFAULT_MAX_SIZE);
		
		updateChart();
	}
	
	public String getToolTipText(MouseEvent event) {
		String result = null;
		if (hoverResource != null) {
			result = "<html>" +
				"<b>Resource</b> <br>" +
				"<b>Farm:</b> " + hoverResource.getFarmName() + "<br>" +
				"<b>Cluster:</b> " + hoverResource.getClusterName() + "<br>" +
				"<b>Node:</b> " + hoverResource.getNodeName() +  
				"</html>";
		}
		
		return result;
	}

	/**
	 * 
	 */
	public void paint(Graphics g) {
		Graphics2D gtd = (Graphics2D)g;
		
		RenderingHints renderHints = null;
		
		if (chartProperties.isAntialiasingEnabled()) {
			renderHints = new RenderingHints(RenderingHints.KEY_ANTIALIASING,
					RenderingHints.VALUE_ANTIALIAS_ON);
		} else {
			renderHints = new RenderingHints(RenderingHints.KEY_ANTIALIASING,
					RenderingHints.VALUE_ANTIALIAS_OFF);
		}
		
		renderHints.put(RenderingHints.KEY_RENDERING,
				RenderingHints.VALUE_RENDER_SPEED);
		
		gtd.setRenderingHints(renderHints);
		
		if (needsMetricsUpdate) {
			updateMetrics();
		}
		
		
		
		paintBackground(gtd);
		paintTitle(gtd);
		paintLegend(gtd);
		
		paintResourcesAxis(gtd);
		paintTimeAxis(gtd);
		
		paintMappings(gtd);
	}
	
	/**
	 * 
	 * @return
	 */
	public TaskScheduleResult getDataSource() {
		return dataSource;
	}
	
	/**
	 * 
	 * @param dataSource
	 */
	public void setDataSource(TaskScheduleResult dataSource) {
		this.dataSource = dataSource;
		
		hoverMapping = null;
		selectedMapping = null;
		
		needsPaintUpdate = true;
		needsMetricsUpdate = true;
		updateChart();
	}
	
	public GanttChartProperties getChartProperties() {
		return (GanttChartProperties)chartProperties.clone();
	}
	
	public void setChartProperties(GanttChartProperties properties) {
		this.chartProperties = (GanttChartProperties)properties.clone();
		
		needsMetricsUpdate = true;
		needsPaintUpdate = true;
		updateChart();
	}
	
	public GanttChartMetrics getPreferredMetrics() {
		return (GanttChartMetrics)preferredMetrics.clone();
	}
	
	public void setPreferredMetrics(GanttChartMetrics metrics) {
		preferredMetrics = (GanttChartMetrics)metrics.clone();
		
		needsMetricsUpdate = true;
		updateChart();
	}
	
	public GanttChartMetrics getChartMetrics() {
		return (GanttChartMetrics)chartMetrics.clone();
	}
}
